""" Worlds and world management system (WMS).
"""

import operator
import weakref
from math import ceil

try:
    import json
except ImportError:
    try:
        import simplejson as json
    except ImportError:
        pass

import pygame
from pygame import Rect

from sbak import (
    error,
    debug,
    resman,
    event,
    draw,
    entity,
)
from sbak.event import (
    Forwarder,
    EventManager
)
from sbak.resman import Resource
from sbak.draw import image as image_draw, fix_color

__all__ = (
    'World',
    'Universe',
    'load',
    'from_json'
)


# ~~~~~~~~ CLASSES ~~~~~~~~

class WorldM(EventManager.__metaclass__, Resource.__metaclass__):
    """ Metaclass for the World class.
    
        Combines the functionality of the metaclasses for
        `event.EventManager` and `resman.Resource`.
    """
    def __init__(cls, name, bases, dict):
        super(WorldM, cls).__init__(name, bases, dict)

class World(EventManager, Resource):
    """ A 2d place in which spatial objects can exist.
        
        ~~~ Attributes ~~~
        
        bgcolor:    If not None this will be used as a background color
                    to fill the target surface with before drawing
                    other objects.
    """
    
    __metaclass__ = WorldM
    
    def __init__(   self,   region = (0,0,0,0),
                            bgcolor = None,
                            entities = (),
                            # Alternative to region
                            size = None):
        """ Initializes the world object.
            
            region: pass a pygame Rect or compatible. The x and y
                    values will be the world's absolute position in the
                    universe and the width and height values will be
                    the size of the world in pixels.
                    NOTE: World position is currently unused.
                    NOTE: The default value of (0,0,0,0) is not a
                    good value to use unless you plan to completly
                    ignore the world's size (in other words, pretend
                    that the world is infititely large).
            
            bgcolor:
                    You can specify a background color to fill surfaces
                    with whenever a world is drawn on them. None means
                    the surface is not filled with a color.
            
            entities:
                    A sequence of entities to put into the world.
                    Be careful; remember that entities added to a
                    world are removed from whatever world they were
                    previously a part of. (Also remember that the
                    second argument for most entity constructors
                    is the world; however, you won't have to supply
                    this if you create entities from inside the
                    `entities` argument since the world will add the
                    entities to the world anyway.)
            
            size:   If you don't want to use the (x,y) component of the
                    region you can specify a size as a (w,h) pair using
                    this argument. If it is given, this will be used
                    in place of the region's size.
        """
        EventManager.__init__(self, entities)
        Resource.__init__(self)
        
        # Absolute position and size of world in pixels.
        region = Rect(region)
        if size is not None:
            region[2:4] = size
        self._region = region
        
        # Color used to fill the background, if any
        self.bgcolor = fix_color(bgcolor) if bgcolor is not None else None
    
    def get_universe(self):
        """ Returns the universe that the world is in.
        
            If the world is not in a universe, this returns None.
        """
        try:
            return self.parent.get_universe()
        except AttributeError:
            return None
    
    def draw(self, dest=None, pos=(0,0), region=None):
        """ Draws the world.
            
            dest:   Pass a pygame surface object. (Passing None is
                    assumed to mean the screen surface, but it is
                    actually up to the entities to decide what it
                    means.)
                    
            pos:    Pass a sequence of 2 ints. This is the position
                    on the surface at which the world will be drawn.
                    If a region is given (see below) this is the
                    position at which the region will be drawn.
            
            region: Pass a pygame Rect or compatible object. If given,
                    this region will be passed on to entities' draw
                    methods. It is up to the entities to decide what to
                    do with the region. (The average entity simply
                    does not draw itself when its image does not
                    overlap with the region, while tilemaps draw only
                    the tiles which are overlapping the region.)
            
            This will draw all entities in the world in order from
            lowest to highest layer. Hidden entities are not drawn.
            
            TODO: Return a list of affected areas?
            TODO: Allow for clipping?
        """
        # Draw a background color if one is present
        if self.bgcolor is not None:
            reg = (self.region.clip(region) if region is not None
                   else self.region).move(pos)
            dest.fill(self.bgcolor, reg)
            
        # Determine which entities should be drawn based on hiddenness.
        drawlist = []
        for e in self:
            if not e.hidden:
                drawlist.append(e)
        
        # Draw all non-hidden entities in order from lowest to highest layer.
        for e in sorted(drawlist, lambda a,b: int(a.layer)-int(b.layer)):
            e.draw(dest, pos, region)

    def find_collider(self, rect, exclude=None):
        """ Look for a collision between a rect and an entity.
        
            rect:       A pygame Rect object or compatible sequence.
            
            exclude:    An entity to exclude from the search.
            
            This is the simplest collision detection method that the
            world provides. It merely asks each solid entity in the
            world if it is colliding with the given rectangle, and if
            so, returns the entity. If no entity is found, None is
            returned.
        """
        for e in self:
            if e is not exclude and e.solid and e.collide(rect):
                return e
    
    def find_collider_group(self, rect, exclude=None, group=None):
        """ Like find_collider_rect, but only matches entities in a group.
        
            rect:       A pygame Rect object or compatible sequence.
            
            exclude:    An entity to exclude from the search.
            
            group:      An optional group to require entities to be in.
                        This can be None (any group membership), a
                        pygame.sprite.Group object (or compatible), or
                        a string. If it is a string, it is converted to
                        a DynamicGroup using `group.get()`. Entities
                        not in this group will not match the search.
            
            Returns an entity or None.
        """
        for e in self:
            if e is not exclude and e.solid and e.collide_group(rect, group):
                return e
    
    def find_collider_groups(self, rect, exclude=None, groups=()):
        """ Like find_collider_group, but allows multiple groups.
        
            rect:       A pygame Rect object or compatible sequence.
            
            exclude:    An entity to exclude from the search.
            
            groups:     An optional sequence of groups to require
                        entities to be in. Each item may be a
                        pygame.sprite.Group object (or compatible)
                        or a string. Each string group is converted
                        to a DynamicGroup using `group.get()`. If the
                        entity is not a member of every group in the
                        sequence the match will fail.
            
            Returns an entity or None.
        """
        for e in self:
            if e is not exclude and e.solid and e.collide_groups(rect, groups):
                return e
            
    def find_colliders(self, rect, exclude=None):
        """ Look for collisions between a rect and entities.
        
            rect:       A pygame Rect object or compatible sequence.
            
            exclude:    An entity to exclude from the search.
            
            This is like find_collider, but returns all entities that
            collide with the rect instead of just the first.
            
            Returns a list of 0 or more entities.
        """
        colliders=[]
        for e in self:
            if e is not exclude and e.solid and e.collide(rect):
                colliders.append(e)
        return colliders
    
    def find_colliders_group(self, rect, exclude=None, group=None):
        """ Like find_colliders_rect, but only matches entities in a group.
        
            rect:       A pygame Rect object or compatible sequence.
            
            exclude:    An entity to exclude from the search.
            
            group:      An optional group to require entities to be in.
                        This can be None (any group membership), a
                        pygame.sprite.Group object (or compatible), or
                        a string. If it is a string, it is converted to
                        a DynamicGroup using `group.get()`. Entities
                        not in this group will not match the search.
                        
            Returns a list of 0 or more entities.
        """
        colliders=[]
        for e in self:
            if e is not exclude and e.solid and e.collide_group(rect, group):
                colliders.append(e)
        return colliders
    
    def find_colliders_groups(self, rect, exclude=None, groups=()):
        """ Like find_colliders_group, but allows multiple groups.
        
            rect:       A pygame Rect object or compatible sequence.
            
            exclude:    An entity to exclude from the search.
            
            groups:     An optional sequence of groups to require
                        entities to be in. Each item may be a
                        pygame.sprite.Group object (or compatible)
                        or a string. Each string group is converted
                        to a DynamicGroup using `group.get()`. If the
                        entity is not a member of every group in the
                        sequence the match will fail.
            
            Returns a list of 0 or more entities.
        """
        colliders = []
        for e in self:
            if e is not exclude and e.solid and e.collide_groups(rect, groups):
                colliders.append(e)
        return colliders

    def to_json(self, resman=None, usenone=False):
        """ Returns a JSON-compatable dict representing the world. """
        d = {
            'region' : list(self.region),
            'bgcolor' : list(self.bgcolor) if self.bgcolor is not None else None,
            'entities' : list(e.to_json(resman, usenone) for e in self)
        }
        
        return d

    def get_pos(self):
        return self._region[:2]
    def set_pos(self,val):
        self._region[:2] = val[:2]
    pos = property(get_pos, set_pos,
        """ Position of the world in the universe. """)
    
    def get_x(self):
        return self._region[0]
    def set_x(self,val):
        self._region[0] = val
    x = left = property(get_x, set_x,
        """ X position of the world in the universe. """)
    
    def get_y(self):
        return self._region[1]
    def set_y(self,val):
        self._region[1] = val
    y = top = property(get_y, set_y,
        """ Y position of the world in the universe. """)

    def get_w(self):
        return self._region[2]
    def set_w(self,w):
        self._region[2] = int(w)
    width = w = property(get_w,set_w,None,
        """ Width of the world in pixels. """)
    
    def get_h(self):
        return self._region[3]
    def set_h(self,h):
        self._region[3] = int(h)
    height = h = property(get_h,set_h,None,
        """ Height of the world in pixels. """)
    
    def get_region(self):
        return self._region
    def set_region(self,val):
        self._region = Rect(val)
    region = property(get_region, set_region, doc=
        """ Returns the area occupied the world as a rect. """)

class Universe(Forwarder):
    """ Primary container type for worlds in a game.
    
        A universe is the all-encompasing "super space" into which all
        worlds of a game should go. It exists mainly to separate the
        worlds from other components of the game or other app, such as
        inputs.
        
        It is assumed that only one universe will exist per app.
        Children of the universe (worlds) and grandchildren (entities)
        as well as the parent (game or app) will expect this, so
        having more than one can cause undefined behavior.
        
        A universe's ID is automatically set to "universe" upon
        creation. This is because the `universe` property of the
        `sbak.app.Game` class searches for the first child in the
        game that has "universe" for its ID. Therefore, it is best to
        leave this ID as is.
    """
    
    tag = "universe"
    
    def __init__(self, worlds=()):
        super(Universe,self).__init__(worlds)
        self.id = "universe"
    
    def get_universe(self):
        """ Returns self.
        
            This is used by worlds and entities to determine the
            universe they are in.
        """
        return self
    

# ~~~~~~~~ FUNCTIONS ~~~~~~~~

def from_json(obj, resmgr=None):
    """ Creates a world from a JSON object or JSON-compatible file.
    
        obj:    A mapping of JSON-compatible objects.
        
        resmgr: A resource manager to use to load other resources. If
                not given, the global resource manager is used.
    """
    
    wld = World(
        region = obj['region'] if 'region' in obj else (0,0,0,0),
        bgcolor = obj['bgcolor'] if 'bgcolor' in obj else None,
        paused = obj['paused'] if 'paused' in obj else False,
        entities = [entity.from_json(e, resmgr) for e in obj['entity']]
    )
    return wld

def load(src, resmgr=None):
    """ Load a world from a JSON file.
    
        src:    A filename or file-like object. This will be loaded
                (if necessary) and parsed as JSON code. The resulting
                dictionary will then be passed to world.from_json().
        
        resmgr: A resource manager to use to load other resources. If
                not given, the global resource manager is used.
    """
    
    # If the src is a string, assume it is a filename and open it.
    close = False
    if isinstance(src, basestring):
        close = True
        src = open(src)
    
    # Parse the file with JSON.
    wld = json.load(src)
    
    # Close the file if necessary
    if close:
        src.close()
        
    # Turn the JSON into a world
    wld = from_json(wld, resmgr)
    
    return wld
